﻿using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Threading;

namespace Microsoft.Coding4Fun.TweetCraft.Twitter
{
    /// <summary>
    /// Implements the TweetCraft Twitter background service using TwitterLib from Witty Twitter.
    /// </summary>
    public sealed class TwitterBackgroundService : TwitterService, ITwitterBackgroundService, IDisposable
    {
        private readonly static TimeSpan MinimumPollingInterval = TimeSpan.FromSeconds(30);
        private readonly BackgroundWorker pollingWorker = new BackgroundWorker();
        private readonly ITwitterClient twitterClient;

        private readonly object syncObject = new object();

        private DateTime lastPollingDate = DateTime.MinValue;

        private volatile bool pollingEnabled;
        private volatile bool poll;

        private Tweet[] tweets, replies;

        /// <summary>
        /// Initializes a new instance of the <see cref="TwitterBackgroundService"/> class.
        /// </summary>
        /// <param name="twitterClient">The Twitter service.</param>
        public TwitterBackgroundService(ITwitterClient twitterClient)
        {
            if (twitterClient == null)
                throw new ArgumentNullException("twitterClient");

            this.twitterClient = twitterClient;
            this.InitializePollingWorker();

            // Initialize defaults (enforce minimum polling interval)
            this.PollingInterval = MinimumPollingInterval > Settings.Default.PollingInterval ? MinimumPollingInterval : Settings.Default.PollingInterval;
        }

        private void InitializePollingWorker()
        {
            // Enable progress reporting and cancellation, hook up polling worker to DoPollingWork
            // and raise TweetsChanged event when the polling worker reports progress
            pollingWorker.WorkerReportsProgress = true;
            pollingWorker.WorkerSupportsCancellation = true;
            pollingWorker.DoWork += DoPollingWork;
            pollingWorker.ProgressChanged += (sender, e) => this.OnTweetsChanged();
        }

        /// <summary>
        /// Gets the latest tweets from the Friends or Public timeline.
        /// </summary>
        public ReadOnlyCollection<Tweet> Tweets
        {
            get
            {
                lock (syncObject)
                {
                    // Do not retrieve tweets synchronously, return empty array instead
                    if (tweets == null)
                        return new ReadOnlyCollection<Tweet>(new Tweet[0]);

                    return new ReadOnlyCollection<Tweet>(tweets);
                }
            }
        }

        /// <summary>
        /// Gets the latest replies for the user.
        /// </summary>
        public ReadOnlyCollection<Tweet> Replies
        {
            get
            {
                lock (syncObject)
                {
                    // Do not retrieve tweets synchronously, return empty array instead
                    if (replies == null)
                        return new ReadOnlyCollection<Tweet>(new Tweet[0]);

                    return new ReadOnlyCollection<Tweet>(replies);
                }
            }
        }

        /// <summary>
        /// Gets or sets a value indicating whether Twitter is polled for new tweets periodically.
        /// </summary>
        /// <value></value>
        public bool PollingEnabled
        {
            get { return pollingEnabled; }
            set
            {
                if (pollingEnabled != value)
                {
                    pollingEnabled = value;
                    InitializePolling();
                }

            }
        }


        /// <summary>
        /// Gets or sets the <see cref="TimeSpan"/> between polling the Twitter service.
        /// </summary>
        public TimeSpan PollingInterval { get; set; }

        /// <summary>
        /// Raised when new tweets or replies are available as part of polling Twitter.
        /// </summary>
        public event EventHandler TweetsChanged;

        /// <summary>
        /// Polls the Twitter service.
        /// </summary>
        public void Poll()
        {
            // Perform the polling
            this.PollInternal();
        }

        /// <summary>
        /// Trigger an immediate polling.
        /// </summary>
        /// <remarks>
        /// The polling still occurs in the background and <see cref="PollingEnabled"/>
        /// must be set to true.
        /// </remarks>
        public void PollAsync()
        {
            if (!this.PollingEnabled)
                throw new InvalidOperationException(Strings.CannotPollPollingDisabled);

            poll = true;
        }

        /// <summary>
        /// Raises the <see cref="TweetsChanged"/> event.
        /// </summary>
        private void OnTweetsChanged()
        {
            if (TweetsChanged != null)
                TweetsChanged(this, EventArgs.Empty);
        }

        private void InitializePolling()
        {
            if (pollingEnabled)
            {
                // Polling should be enabled, instruct polling worker to start doing its work
                if (!pollingWorker.IsBusy)
                    pollingWorker.RunWorkerAsync();
            }
            else
            {
                // Polling should be disabled, if polling worker is busy, cancel background operation
                if (pollingWorker.IsBusy)
                    pollingWorker.CancelAsync();
            }
        }

        private void DoPollingWork(object sender, DoWorkEventArgs e)
        {
            // Work while there is no cancellation pending
            while (!pollingWorker.CancellationPending)
            {
                // Check whether the polling interval has elapsed since the last polling date 
                // or a poll has been triggered
                if (DateTime.Now > lastPollingDate + this.PollingInterval || poll)
                {
                    if (poll)
                        Trace.TraceInformation("Polling has been explicitly requested, performing poll...");
                    else
                        Trace.TraceInformation("The polling interval has elapsed since the last poll, performing poll...");

                    // Acknowledge explicit poll
                    poll = false;

                    // Perform polling
                    PollInternal();
                }
                else
                {
                    // Spin idle for a second
                    Thread.Sleep(TimeSpan.FromSeconds(1));
                }
            }
        }

        private void PollInternal()
        {
            try
            {
                using (TraceScope scope = TraceScope.Information("Polling Twitter API for tweets and replies..."))
                {
                    // Retrieve tweets from Twitter
                    Tweet[] latestTweets = Settings.Default.RetrievePublicTimeline
                                           ? twitterClient.GetPublicTimeline()
                                           : twitterClient.GetFriendsTimeline();
                                       

                    // Retrieve replies from Twitter
                    Tweet[] latestReplies = twitterClient.GetMentions();

                    Trace.TraceInformation("{0} tweets and {1} replies available through Twitter API.", latestTweets.Length, latestReplies.Length);

                    // Synchronize access to tweets and replies with other threads
                    lock (syncObject)
                    {
                        // Check whether the latest tweet / reply is different from the ones we have
                        if (latestTweets.GetLatestTweet() != tweets.GetLatestTweet() || 
                            latestReplies.GetLatestTweet() != replies.GetLatestTweet())
                        {
                            Trace.TraceInformation("Tweets and/or replies have been updated since last polling.");

                            // Update tweets and replies and report progress
                            this.tweets = latestTweets;
                            this.replies = latestReplies;

                            pollingWorker.ReportProgress(0);
                        }
                    }

                    scope.Complete("Twitter API polling is complete.");
                }
            }
            catch (TwitterException exception)
            {
                // Report the error, but keep running
                this.OnError(exception);
            }

            // Update last polling date
            lastPollingDate = DateTime.Now;
        }

        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        public void Dispose()
        {
            if (pollingWorker != null)
                pollingWorker.Dispose();

            GC.SuppressFinalize(this);
        }
    }
}